perm filename FLAVOR.DOC[MAC,LSP] blob
sn#579636 filedate 1981-04-17 generic text, type C, neo UTF8
COMMENT ⊗ VALID 00002 PAGES
C REC PAGE DESCRIPTION
C00001 00001
C00002 00002 .c -*- Mode: Bolio Lowercase: T -*-
C00164 ENDMK
C⊗;
.c -*- Mode: Bolio; Lowercase: T -*-
.c This is a draft of a new chapter for the LMMAN. DLW 10/1/80
.c I assume that this comes after the discussion of defstruct (chap 17) but
.c before the discussion of streams (sect 18.5).
.chapter Objects, Message Passing, and Flavors
.setq flavor-chapter chapter-number
.cindex flavor
.cindex message
.cindex method
.cindex object
.cindex instance
.setq flavor section-page
.section Introduction
The object oriented programming style used in the Smalltalk and
Actor families of languages is available in Lisp Machine Lisp, and used
by the Lisp Machine software system. Its purpose is to perform
ε2generic operationsε* on objects. Part of its implementation is
simply a convention in procedure calling style; part is a powerful
language feature, called Flavors, for defining abstract objects.
This chapter attempts to explain what programming with objects and with
message passing means, the various means of implementing these in Lisp
Machine Lisp, and when you should use them. It assumes no prior
knowledge of any other languages.
.section Objects
When writing a program, it is often convenient to model what the program
does in terms of ε2objectsε*: conceptual entities that can be likened to
real-world things. Choosing what objects to provide in a program is
very important to the proper organization of the program. In an
object-oriented design, specifying what objects exist is the first
task in designing the system. In a text editor, the objects might be
"pieces of text", "pointers into text", and "display windows". In an
electrical design system, the objects might be "resistors",
"capacitors", "transistors", "wires", and "display windows". After
specifying what objects there are, the next task of the design is to
figure out what operations can be performed on each object. In the text
editor example, operations on "pieces of text" might include inserting
text and deleting text; operations on "pointers into text" might include
moving forward and backward; and operations on "display windows" might
include redisplaying the window and changing with which "piece of text" the
window is associated.
In this model, we think of the program as being built around a set of
objects, each of which has a set of operations that can be performed on
it. More rigorously, the program defines several ε2typesε* of object
(the editor above has three types), and it can create many ε2instancesε*
of each type (that is, there can be many pieces of text, many pointers
into text, and many windows). The program defines a
set of types of object, and the operations that can be performed on
any of the instances of each type.
This should not be wholly unfamiliar to the reader. Earlier in this
manual, we saw a few examples of this kind of programming. A simple
example is disembodied property lists, and the functions ε3getε*,
ε3putpropε*, and ε3rempropε*. The disembodied property list is a type of
object; you can instantiate one with ε3(cons nil nil)ε* (that is, by
evaluating this form you can create a new disembodied property list);
there are three operations on the object, namely ε3getε*, ε3putpropε*,
and ε3rempropε*. Another example in the manual was the first example of
the use of ε3defstructε*, which was called a ε3shipε*. ε3defstructε*
automatically defined some operations on this object: the operations to
access its elements. We could define other functions that did useful
things with ε3shipε*s, such as computing their speed, angle of travel,
momentum, or velocity, stopping them, moving them elsewhere, and so on.
In both cases, we represent our conceptual object by one Lisp object.
The Lisp object we use for the representation has ε2structureε*, and refers
to other Lisp objects. In the property list case, the Lisp object is a
list with alternating indicators and values; in the ε3shipε* case, the
Lisp object is an array whose details are taken care of by
ε3defstructε*. In both cases, we can say that the object keeps track of
an ε2internal stateε*, which can be ε2examinedε* and ε2alteredε* by the
operations available for that type of object. ε3getε* examines the state
of a property list, and ε3putpropε* alters it; ε3ship-x-positionε*
examines the state of a ship, and ε3(setf (ship-mass) 5.0)ε* alters it.
We have now seen the essence of object-oriented programming. A
conceptual object is modelled by a single Lisp object, which bundles up
some state information. For every type of object, there is a set of
operations that can be performed to examine or alter the state of the
object.
.section Modularity
An important benefit of the object-oriented style is that it lends
itself to a particularly simple and lucid kind of modularity.
If you have modular programming constructs and techniques available,
it helps and encourages you to write
programs that are easy to read and understand, and so are more
reliable and maintainable. Object-oriented programming lets a
programmer implement a useful facility that presents the caller with a set
of external interfaces, without requiring the caller to understand how
the internal details of the implementation work. In other words, a program
that calls this facility can treat the facility as a black box; the
program knows what the facility's external interfaces guarantee to do,
and that is all it knows.
For example, a program that uses disembodied property lists never needs
to know that the property list is being maintained as a list of
alternating indicators and values; the program simply performs the
operations, passing them inputs and getting back outputs. The program
only depends on the external definition of these operations: it knows
that if it ε3putpropε*s a property, and doesn't ε3rempropε* it (or
ε3putpropε* over it), then it can do ε3getε* and be sure of getting back
the same thing it put in. The important thing about this hiding of
the details of the implementation is that someone reading a program that uses disembodied
property lists need not concern himself with how they are implemented;
he need only understand what they undertake to do. This saves the
programmer a lot of time, and lets him concentrate his energies on
understanding the program he is working on. Another good thing about
this hiding is that the representation of property lists could be
changed, and the program would continue to work. For example, instead
of a list of alternating elements, the property list could be
implemented as an association list or a hash table. Nothing in the
calling program would change at all.
The same is true of the ε3shipε* example. The caller is presented with
a collection of operations, such as ε3ship-x-positionε*,
ε3ship-y-positionε*, ε3ship-speedε*, and ε3ship-directionε*; it simply
calls these and looks at their answers, without caring how they did what
they did. In our example above, ε3ship-x-positionε* and
ε3ship-y-positionε* would be accessor functions, defined automatically
by ε3defstructε*, while ε3ship-speedε* and ε3ship-directionε* would be
functions defined by the implementor of the ε3shipε* type. The code
might look like this:
.lisp
(defstruct (ship)
ship-x-position
ship-y-position
ship-x-velocity
ship-y-velocity
ship-mass)
(defun ship-speed (ship)
(sqrt (+ (↑ (ship-x-velocity ship) 2)
(↑ (ship-y-velocity ship) 2))))
(defun ship-direction (ship)
(atan (ship-y-velocity ship)
(ship-x-velocity ship)))
.end←lisp
The caller need not know that the first two functions were structure accessors
and that the second two were written by hand and do arithmetic. Those
facts would not be considered part of the black box characteristics of
the implementation of the ε3shipε* type. The ε3shipε* type does not
guarantee which functions will be implemented in which ways; such aspects
are not part of the contract between ε3shipε* and its callers. In fact,
ε3shipε* could have been written this way instead:
.lisp
(defstruct (ship)
ship-x-position
ship-y-position
ship-speed
ship-direction
ship-mass)
(defun ship-x-velocity (ship)
(* (ship-speed ship) (cos (ship-direction ship))))
(defun ship-y-velocity (ship)
(* (ship-speed ship) (sin (ship-direction ship))))
.end←lisp
In this second implementation of the ε3shipε* type, we have decided to
store the velocity in polar coordinates instead of rectangular
coordinates. This is purely an implementation decision; the caller has
no idea which of the two ways the implementation works, because he just
performs the operations on the object by calling the appropriate
functions.
We have now created our own types of objects, whose implementations are
hidden from the programs that use them. Such types are usually referred to as
ε2abstract typesε*. The object-oriented style of programming can be
used to create abstract types by hiding the implementation of the
operations, and simply documenting what the operations are defined to do.
Some more terminology: the quantities being held by the elements of the
ε3shipε* structure are referred to as ε2instance variablesε*. Each
instance of a type has the same operations defined on it; what
distinguishes one instance from another (besides identity (ε3eqε*ness))
is the values that reside in its instance variables. The example above
illustrates that a caller of operations does not know what the instance
variables are; our two ways of writing the ε3shipε* operations have
different instance variables, but from the outside they have exactly the
same operations.
One might ask: "But what if the caller evaluates ε3(aref ship 2)ε* and
notices that he gets back the x-velocity rather than the speed? Then he
can tell which of the two implementations were used." This is true; if
the caller were to do that, he could tell. However, when a facility is
implemented in the object-oriented style, only certain functions are
documented and advertised: the functions which are considered to be
operations on the type of object. The contract from ε3shipε* to its
callers only speaks about what happens if the caller calls these
functions. The contract makes no guarantees at all about what would
happen if the caller were to start poking around on his own using
ε3arefε*. A caller who does so ε2is in errorε*; he is depending on
something that is not specified in the contract. No guarantees were
ever made about the results of such action, and so anything may happen;
indeed, ε3shipε* may get reimplemented overnight, and the code that does
the ε3arefε* will have a different effect entirely and probably stop
working. This example shows why the concept of a contract between a callee and
a caller is important: the contract is what specifies the interface
between the two modules.
Unlike some other languages that provide abstract types, Lisp Machine
Lisp makes no attempt to have the language automatically forbid
constructs that circumvent the contract. This is intentional. One
reason for this is that the Lisp Machine is an interactive system, and
so it is important to be able to examine and alter internal state
interactively (usually from a debugger). Furthermore, there is no
strong distinction between the "system" programs and the "user" programs
on the Lisp Machine; users are allowed to get into any part of the
language system and change what they want to change.
In summary: by defining a set of operations, and making only a
specific set of external entrypoints available to the caller, the
programmer can create his own abstract types. These types can be useful
facilities for other programs and programmers. Since the implementation
of the type is hidden from the callers, modularity is maintained, and
the implementation can be changed easily.
We have hidden the implementation of an abstract type by making its
operations into functions which the user may call. The important thing
is not that they are functions--in Lisp everything is done with functions.
The important thing is that we have defined a new conceptual operation
and given it a name, rather than requiring anyone who wants to do the
operation to write it out step-by-step. Thus we say ε3(ship-x-velocity s)ε*
rather than ε3(aref s 2)ε*.
It is just as true of such abstract-operation functions as of ordinary
functions that sometimes they are simple enough that we want the compiler
to compile special code for them rather than really calling the function.
(Compiling special code like this is often called ε2open-codingε*.)
The compiler is directed to do this through use of macros, defsubsts, or
optimizers. ε3defstructε* arranges for this kind of special compilation
for the functions that get the instance variables of a structure.
When we use this optimization, the implementation of the abstract type
is only hidden in a certain sense. It does not appear in the Lisp code
written by the user, but does appear in the compiled code. The reason
is that there may be some compiled functions that use the macros (or whatever);
even if you change the definition of the macro, the existing compiled
code will continue to use the old definition. Thus, if
the implementation of a module is changed programs that use it may need to be
recompiled. This is something we sometimes accept for the sake of
efficiency.
In the present implementation of flavors, which is discussed below, there is
no such compiler incorporation of nonmodular knowledge into a
program, except when the "outside-accessible instance variables"
feature is used; see ⊗(outside-accessible-instance-variables-option),
where this problem is explained further. If you don't use the "outside-accessible instance
variables" feature, you don't have to worry about this.
.section Generic Operations
Suppose we think about the rest of the program that uses the
ε3shipε* abstraction. It may want to deal with other objects that are
like ε3shipε*s in that they are movable objects with mass, but unlike
ε3shipε*s in other ways. A more advanced model of a ship might include
the concept of the ship's engine power, the number of passengers on
board, and its name. An object representing a meteor probably would
not have any of these, but might have another attribute such as how
much iron is in it.
However, all kinds of movable objects have positions, velocities, and
masses, and the system will contain some programs that deal with these
quantities in a uniform way, regardless of what kind of object the
attributes apply to. For example, a piece of the system that calculates
every object's orbit in space need not worry about the other, more
peripheral attributes of various types of objects; it works the same way
for all objects. Unfortunately, a program that tries to calculate the
orbit of a ship will need to know the ship's attributes, and will have
to call ε3ship-x-positionε* and ε3ship-y-velocityε* and so on. The problem is
that these functions won't work for meteors. There would have to be a
second program to calculate orbits for meteors that would be exactly the
same, except that where the first one calls ε3ship-x-positionε*, the
second one would call ε3meteor-x-positionε*, and so on. This would be
very bad; a great deal of code would have to exist in multiple copies,
all of it would have to be maintained in parallel, and it would take up
space for no good reason.
What is needed is an operation that can be performed on objects of
several different types. For each type, it should do the thing
appropriate for that type. Such operations are called ε2genericε*
operations. The classic example of generic operations is the arithmetic
functions in most programming languages, including Lisp Machine Lisp.
The ε3+ε* (or ε3plusε*) function will accept either fixnums or flonums,
and perform either fixnum addition or flonum addition, whichever is
appropriate, based on the data types of the objects being manipulated.
In our example, we need a generic ε3x-positionε* operation that can be
performed on either ε3shipε*s, ε3meteorε*s, or any other kind of mobile
object represented in the system. This way, we can write a single
program to calculate orbits. When it wants to know the ε2xε* position
of the object it is dealing with, it simply invokes the generic
ε3x-positionε* operation on the object, and whatever type of object it
has, the correct operation is performed, and the ε2xε* position is
returned.
A terminology for the use of such generic operations has emerged from
the Smalltalk and Actor languages: performing a generic operation is
called ε2sending a messageε*. The objects in the program are thought of
as little people, who get sent messages and respond with answers. In
the example above, the objects are sent ε3x-positionε* messages, to
which they respond with their ε2xε* position. This ε2message passingε*
is how generic operations are performed.
Sending a message is a way of invoking a function. Along with the
ε2nameε* of the message, in general, some arguments are passed; when the
object is done with the message, some values are returned. The sender
of the message is simply calling a function with some arguments, and
getting some values back. The interesting thing is that the caller did
not specify the name of a procedure to call. Instead, it specified a
message name and an object; that is, it said what operation to
perform, and what object to perform it on. The function to invoke
was found from this information.
When a message is sent to an object, a function therefore must be found
to handle the message. The two data used to figure out which function
to call are the ε2typeε* of the object, and the ε2nameε* of the message.
The same set of functions are used for all instances of a given type, so
the type is the only attribute of the object used to figure out which
function to call. The rest of the message besides the name are data
which are passed as arguments to the function, so the name is the only
part of the message used to find the function. Such a function is
called a ε2methodε*. For example, if we send an ε3x-positionε* message
to an object of type ε3shipε*, then the function we find is "the
ε3shipε* type's ε3x-positionε* method". A method is a function that
handles a specific kind of message to a specific kind of object; this
method handles messages named ε3x-positionε* to objects of type
ε3shipε*.
In our new terminology: the orbit-calculating program finds the ε2xε*
position of the object it is working on by sending that object a message
named ε3x-positionε* (with no arguments). The returned value of the
message is the ε2xε* position of the object. If the object was of type
ε3shipε*, then the ε3shipε* type's ε3x-positionε* method was invoked; if
it was of type ε3meteorε*, then the ε3meteorε* type's ε3x-positionε*
method was invoked. The orbit-calculating program just sends the
message, and the right function is invoked based on the type of the
object. We now have true generic functions, in the form of message
passing: the same operation can mean different things depending on the
type of the object.
.section Generic Operations in Lisp
How do we implement message passing in Lisp? By convention, objects
that receive messages are always ε2functionalε* objects (that is, you
can apply them to arguments), and a message is sent to an object by
calling that object as a function, passing the name of the message as
the first argument, and the arguments of the message as the rest of the
arguments. Message names are represented by symbols; normally these
symbols are in the keyword package (see ⊗(package)) since messages are
a protocol for communication between different programs, which may
reside in different packages. So if we have a variable ε3my-shipε*
whose value is an object of type ε3shipε*, and we want to know its
ε2xε* position, we send it a message as follows:
.lisp
(funcall my-ship ':x-position)
.end←lisp
This form returns the ε2xε* position as its returned value. To set the
ship's ε2xε* position to ε33.0ε*, we send it a message like this:
.lisp
(funcall my-ship ':set-x-position 3.0)
.end←lisp
It should be stressed that no new features are added to Lisp for message
sending; we simply define a convention on the way objects take
arguments. The convention says that an object accepts messages by
always interpreting its first argument as a message name. The object
must consider this message name, find the function which is the method
for that message name, and invoke that function.
This raises the question of how message receiving works. The object
must somehow find the right method for the message it is sent.
Furthermore, the object now has to be callable as a function; objects
can't just be ε3defstructsε* any more, since those aren't functions. But
the structure defined by ε3defstructε* was doing something useful: it
was holding the instance variables (the internal state) of the object.
We need a function with internal state; that is, we need a coroutine.
Of the Lisp Machine Lisp features presented so far, the most appropriate
is the closure (see ⊗(closure)). A message-receiving object could be
implemented as a closure over a set of instance variables. The function
inside the closure would have a big ε3selectqε* form to dispatch on its
first argument. (Actually, rather than using closures and a ε3selectqε*,
the Lisp Machine provides ε2entitiesε* and ε3defselectε*; see ⊗(entity).)
.setq entity-usage page
While using closures (or entities) does work, it has several serious
problems. The main problem is that in order to add a new operation to a
system, it is necessary to modify a lot of code; you have to find all
the types that understand that operation, and add a new clause to the
ε3selectqε*. The problem with this is that you cannot textually
separate the implementation of your new operation from the rest of the
system; the methods must be interleaved with the other operations for
the type. Adding a new operation should only require ε2addingε* Lisp code;
it should not require ε2modifyingε* Lisp code.
The conventional way of making generic operations is to have a procedure
for each operation, which has a big ε3selectqε* for all the types; this
means you have to modify code to add a type. The way described above is
to have a procedure for each type, which has a big ε3selectqε* for all
the operations; this means you have to modify code to add an operation.
Neither of these has the desired property that extending the system
should only require adding code, rather than modifying code.
Closures (and entities) are also somewhat clumsy and crude. A far more
streamlined, convenient, and powerful system for creating
message-receiving objects exists; it is called the ε2Flavorε* mechanism.
With flavors, you can add a new method simply by adding code, without
modifying anything. Furthermore, many common and useful things to do
are very easy to do with flavors. The rest of this chapter describes
flavors.
.section Simple Use of Flavors
A ε2flavorε*, in its simplest form, is a definition of an abstract type.
New flavors are created with the ε3defflavorε* special form, and
methods of the flavor are created with the ε3defmethodε* special form.
New instances of a flavor are created with the ε3make-instanceε*
function. This section explains simple uses of these forms.
For an example of a simple use of flavors, here is how the ε3shipε*
example above would be implemented.
.lisp
(defflavor ship (x-position y-position
x-velocity y-velocity mass)
()
:gettable-instance-variables)
(defmethod (ship :speed) ()
(sqrt (+ (↑ x-velocity 2)
(↑ y-velocity 2))))
(defmethod (ship :direction) ()
(atan y-velocity x-velocity))
.end←lisp
The code above creates a new flavor. The first subform of the
ε3defflavorε* is ε3shipε*, which is the name of the new flavor. Next
is the list of instance variables; they are the five that should be
familiar by now. The next subform is something we will get to later.
The rest of the subforms are the body of the ε3defflavorε*, and each
one specifies an option about this flavor. In our example, there is
only one option, namely ε3:gettable-instance-variablesε*. This means
that for each instance variable, a method should automatically be
generated to return the value of that instance variable. The name of
the message is a symbol with the same name as the instance variable, but
interned on the keyword package. Thus, methods are created to handle
the messages ε3:x-positionε*, ε3:y-positionε*, and so on.
Each of the two ε3defmethodε* forms adds a method to the flavor. The
first one adds a handler to the flavor ε3shipε* for messages named
ε3:speedε*. The second subform is the lambda-list, and the rest is the
body of the function that handles the ε3:speedε* message. The body can
refer to or set any instance variables of the flavor, the same as it can
with local variables or special variables. When any instance of the
ε3shipε* flavor is invoked with a first argument of ε3:directionε*, the
body of the second ε3defmethodε* will be evaluated in an environment in
which the instance variables of ε3shipε* refer to the instance variables
of this instance (the one to which the message was sent). So when the
arguments of ε3atanε* are evaluated, the values of instance variables of
the object to which the message was sent will be used as the arguments.
ε3atanε* will be invoked, and the result it returns will be returned by
the instance itself.
Now we have seen how to create a new abstract type: a new flavor. Every
instance of this flavor will have the five instance variables named in
the ε3defflavorε* form, and the seven methods we have seen (five that
were automatically generated because of the
ε3:gettable-instance-variablesε* option, and two that we wrote
ourselves). The way to create an instance of our new flavor is with the
ε3make-instanceε* function. Here is how it could be used:
.lisp
(setq my-ship (make-instance 'ship))
.end←lisp
This will return an object whose printed representation is:
.lisp
#<SHIP 13731210>
.end←lisp
(Of course, the value of the magic number will vary; it is not
interesting anyway.) The argument to ε3make-instanceε* is,
as you can see, the name of the flavor to be instantiated. Additional
arguments, not used here, are ε2init optionsε*, that is, commands
to the flavor of which we are making an instance, selecting optional
features. This will be discussed more in a moment.
Examination of the flavor we have defined shows that it is quite
useless as it stands, since there is no way to set any of the
parameters. We can fix this up easily, by putting the
ε3:settable-instance-variablesε* option into the ε3defflavorε* form.
This option tells ε3defflavorε* to generate methods for messages named
ε3:set-x-positionε*, ε3:set-y-positionε*, and so on; each such method
takes one argument, and sets the corresponding instance variable to the
given value.
Another option we can add to the ε3defflavorε* is
ε3:initable-instance-variablesε*, to allow us to initialize the values
of the instance variables when an instance is first created.
ε3:initable-instance-variablesε* does not create any methods; instead,
it makes ε2initialization keywordsε* named ε3:x-positionε*,
ε3:y-positionε*, etc., that can be used as init-option arguments to
ε3make-instanceε* to initialize the corresponding instance variables.
The set of init options are sometimes called the ε2init-plistε* because
they are like a property list.
Here is the improved ε3defflavorε*:
.lisp
(defflavor ship (x-position y-position
x-velocity y-velocity mass)
()
:gettable-instance-variables
:settable-instance-variables
:initable-instance-variables)
.end←lisp
All we have to do is evaluate this new ε3defflavorε*, and the existing
flavor definition will be updated and now include the new methods and
initialization options. In fact, the instance we generated a while ago
will now be able to accept these new messages! We can set the mass of
the ship we created by evaluating
.lisp
(funcall my-ship ':set-mass 3.0)
.end←lisp
and the ε3massε* instance variable of ε3my-shipε* will properly get set
to ε33.0ε*. If you want to play around with flavors, it is useful
to know that ε3describeε* of an instance tells you the flavor of the
instance and the values of its instance variables. If we were to evaluate
ε3(describe my-ship)ε* at this point, the following would be printed:
.lisp
#<SHIP 13731210>, an object of flavor SHIP,
has instance variable values:
X-POSITION: unbound
Y-POSITION: unbound
X-VELOCITY: unbound
Y-VELOCITY: unbound
MASS: 3.0
.end←lisp
Now that the instance variables are "initable", we can create another
ship and initialize some of the instance variables using the init-plist.
Let's do that and ε3describeε* the result:
.lisp the
(setq her-ship (make-instance 'ship ':x-position 0.0
':y-position 2.0
':mass 3.5))
==> #<SHIP 13756521>
(describe her-ship)
#<SHIP 13756521>, an object of flavor SHIP,
has instance variable values:
X-POSITION: 0.0
Y-POSITION: 2.0
X-VELOCITY: unbound
Y-VELOCITY: unbound
MASS: 3.5
.end←lisp
A flavor can also establish default initial values for instance
variables. These default values are used when a new instance is created
if the values are not initialized any other way. The syntax for
specifying a default initial value is to replace the name of the
instance variable by a list, whose first element is the name and whose
second is a form to evaluate to produce the default initial value. For
example:
.lisp
(defvar *default-x-velocity* 2.0)
(defvar *default-y-velocity* 3.0)
(defflavor ship ((x-position 0.0)
(y-position 0.0)
(x-velocity *default-x-velocity*)
(y-velocity *default-y-velocity*)
mass)
()
:gettable-instance-variables
:settable-instance-variables
:initable-instance-variables)
(setq another-ship (make-instance 'ship ':x-position 3.4))
(describe another-ship)
#<SHIP 14563643>, an object of flavor SHIP,
has instance variable values:
X-POSITION: 3.4
Y-POSITION: 0.0
X-VELOCITY: 2.0
Y-VELOCITY: 3.0
MASS: unbound
.end←lisp
ε3x-positionε* was initialized explicitly, so the default was ignored.
ε3y-positionε* was initialized from the default value, which was
ε30.0ε*. The two velocity instance variables were initialized from
their default values, which came from two global variables. ε3massε*
was not explicitly initialized and did not have a default
initialization, so it was left unbound.
There are many other options that can be used in ε3defflavorε*, and the
init options can be used more flexibly than just to initialize instance
variables; full details are given later in this chapter. But even with
the small set of features we have seen so far, it is easy to write
object-oriented programs.
.section Mixing Flavors
Now we have a system for defining message-receiving objects so that we
can have generic operations. If we want to create a new type called
ε3meteorε* that would accept the same generic operations as ε3shipε*, we
could simply write another ε3defflavorε* and two more ε3defmethodsε*
that looked just like those of ε3shipε*, and then meteors and ships
would both accept the same operations. ε3shipε* would have some
more instance variables for holding attributes specific to ships,
and some more methods for operations that are not generic, but
are only defined for ships; the same would be true of ε3meteorε*.
However, this would be a a wasteful thing to do. The same code has to
be repeated in several places, and several instance variables have to be
repeated. The code now needs to be maintained in many places, which is
always undesirable. The power of flavors (and the name "flavors") comes
from the ability to mix several flavors and get a new flavor. Since the
functionality of ε3shipε* and ε3meteorε* partially overlap, we can take
the common functionality and move it into its own flavor, which might be
called ε3moving-objectε*. We would define ε3moving-objectε* the same
way as we defined ε3shipε* in the previous section. Then, ε3shipε* and
ε3meteorε* could be defined like this:
.lisp
(defflavor ship (engine-power number-of-passengers name)
(moving-object)
:gettable-instance-variables)
(defflavor meteor (percent-iron) (moving-object)
:initable-instance-variables)
.end←lisp
These ε3defflavorε* forms use the second subform, which we ignored
previously. The second subform is a list of flavors to be combined to
form the new flavor; such flavors are called ε2componentsε*.
Concentrating on ε3shipε* for a moment (analogous things are true of
ε3meteorε*), we see that it has exactly one component flavor:
ε3moving-objectε*. It also has a list of instance variables, which
includes only the ship-specific instance variables and not the ones that
it shares with ε3meteorε*. By incorporating ε3moving-objectε*, the ε3shipε*
flavor acquires all of its instance variables, and so need not name them
again. It also acquires all of ε3moving-objectε*'s methods, too. So
with the new definition, ε3shipε* instances will still accept the
ε3:x-velocityε* and ε3:speedε* messages, and they will do the same thing.
However, the ε3:engine-powerε* message will also be understood (and will
return the value of the ε3engine-powerε* instance variable).
What we have done here is to take an abstract type, ε3moving-objectε*,
and build two more specialized and powerful abstract types on top of it.
Any ship or meteor can do anything a moving object can do, and each also
has its own specific abilities. This kind of building can continue; we
could define a flavor called ε3ship-with-passengerε* that was built on
top of ε3shipε*, and it would inherit all of ε3moving-objectε*'s
instance variables and methods as well as ε3shipε*'s instance variables
and methods. Furthermore, the second subform of ε3defflavorε* can be a
list of several components, meaning that the new flavor should combine
all the instance variables and methods of all the flavors in the list,
as well as the ones ε2thoseε* flavors are built on, and so on. All the
components taken together form a big tree of flavors. A flavor is built
from its components, its components' components, and so on. We
sometimes use the term "components" to mean the immediate components
(the ones listed in the ε3defflavorε*), and sometimes to mean all the
components (including the components of the immediate components and so
on). (Actually, it is not strictly a tree, since some flavors might be
components through more than one path. It is really a directed graph;
it can even be cyclic.)
The order in which the components are combined to form a flavor is
important. The tree of flavors is turned into an ordered list by
performing a ε2top-down, depth-firstε* walk of the tree, including non-terminal nodes
ε2beforeε* the subtrees they head, and eliminating duplicates. For example,
if ε3flavor-1ε*'s immediate components are ε3flavor-2ε* and ε3flavor-3ε*,
and ε3flavor-2ε*'s components are ε3flavor-4ε* and ε3flavor-5ε*, and
ε3flavor-3ε*'s component was ε3flavor-4ε*, then
the complete list of components of ε3flavor-1ε* would be:
.lisp
flavor-1, flavor-2, flavor-4, flavor-5, flavor-3
.end←lisp
The flavors earlier in this list are the more specific, less basic ones;
in our example, ε3ship-with-passengersε* would be first in the list,
followed by ε3shipε*, followed by ε3moving-objectε*. A flavor is always
the first in the list of its own components. Notice that ε3flavor-4ε*
does not appear twice in this list. Only the first occurrence of a
flavor appears; duplicates are removed. (The elimination of duplicates
is done during the walk; if there is a cycle in the directed graph, it
will not cause a non-terminating computation.)
The set of instance variables for the new flavor is the union of all the
sets of instance variables in all the component flavors. If both
ε3flavor-2ε* and ε3flavor-3ε* have instance variables named ε3fooε*,
then ε3flavor-1ε* will have an instance variable named ε3fooε*, and any
methods that refer to ε3fooε* will refer to this same instance variable.
Thus different components of a flavor can communicate with one another
using shared instance variables. (Typically, only one component ever
sets the variable, and the others only look at it.) The default initial
value for an instance variable comes from the first component flavor to
specify one.
.cindex combined-method
.setq combined-method page
The way the methods of the components are combined is the heart of the
flavor system. When a flavor is defined, a single function, called a
ε2combined methodε*, is constructed for each message supported by the
flavor. This function is constructed out of all the methods for that
message from all the components of the flavor. There are many different
ways that methods can be combined; these can be selected by the user
when a flavor is defined. The user can also create new forms of
combination.
There are several kinds of methods, but
so far, the only kinds of methods we have seen are ε2primaryε* methods.
The default way primary methods are combined is that all but the
earliest one provided are ignored. In other words, the combined method
is simply the primary method of the first flavor to provide a primary
method. What this means is that if you are starting with a flavor
ε3fooε* and building a flavor ε3barε* on top of it, then you can
override ε3fooε*'s method for a message by providing your own method.
Your method will be called, and ε3fooε*'s will never be called.
Simple overriding is often useful; if you want to make a new flavor
ε3barε* that is just like ε3fooε* except that it reacts completely
differently to a few messages, then this will work. However, often you
don't want to completely override the base flavor's (ε3fooε*'s) method;
sometimes you want to add some extra things to be done. This is where
combination of methods is used.
The usual way methods are combined is that one flavor provides a primary
method, and other flavors provide ε2daemon methodsε*. The idea
is that the primary method is "in charge" of the main business of
handling the message, but other flavors just want to keep informed
that the message was sent, or just want to do the part of the operation
associated with their own area of responsibility.
When methods are combined, a single primary method is found; it comes
from the first component flavor that has one. Any primary methods
belonging to later component flavors are ignored. This is just
what we saw above; ε3barε* could override ε3fooε*'s primary method
by providing its own primary method.
However, you can define other kinds of methods. In particular, you can define
ε2daemonε* methods. They come in two kinds, ε2beforeε* and ε2afterε*. There is
a special syntax in ε3defmethodε* for defining such methods. Here is an example
of the syntax. To give the ε3shipε* flavor an after-daemon method for the
ε3:speedε* message, the following syntax would be used:
.lisp
(defmethod (ship :after :speed) ()
ε2bodyε*)
.end←lisp
Now, when a message is sent, it is handled by a new function called the
ε2combinedε* method. The combined method first calls all of the before daemons,
then the primary method, then all the after daemons. Each method is passed the
same arguments that the combined method was given. The returned values from the
combined method are the values returned by the primary method; any values
returned from the daemons are ignored. Before-daemons are called in the order
that flavors are combined, while after-daemons are called in the reverse order.
In other words, if you build ε3barε* on top of ε3fooε*, then ε3barε*'s
before-daemons will run before any of those in ε3fooε*, and ε3barε*'s
after-daemons will run after any of those in ε3fooε*.
The reason for this order is to keep the modularity order correct. If
we create ε3flavor-1ε* built on ε3flavor-2ε*; then it should not matter
what ε3flavor-2ε* is built out of. Our new before-daemons go before all
methods of ε3flavor-2ε*, and our new after-daemons go after all methods of
ε3flavor-2ε*. Note that if you have no daemons, this reduces to the
form of combination described above. The most recently added component
flavor is the highest level of abstraction; you build a higher-level
object on top of a lower-level object by adding new components to the
front. The syntax for defining daemon methods can be found in the
description of ε3defmethodε* below.
To make this a bit more clear, let's consider a simple example that is
easy to play with: the ε3:print-selfε* method. The Lisp printer
(i.e⊃. the ε3printε* function; see ⊗(printer)) prints instances of flavors by sending
them ε3:print-selfε* messages. The first argument to the
ε3:print-selfε* message is a stream (we can ignore the others for now),
and the receiver of the message is supposed to print its printed
representation on the stream. In the ε3shipε* example above, the reason
that instances of the ε3shipε* flavor printed the way they did is
because the ε3shipε* flavor was actually built on top of a very basic
flavor called ε3vanilla-flavorε*; this component is provided
automatically by ε3defflavorε*. It was ε3vanilla-flavorε*'s
ε3:print-selfε* method that was doing the printing. Now, if we give
ε3shipε* its own primary method for the ε3:print-selfε* message, then
that method will take over the job of printing completely;
ε3vanilla-flavorε*'s method will not be called at all. However, if we
give ε3shipε* a before-daemon method for the ε3:print-selfε* message,
then it will get invoked before the ε3vanilla-flavorε* message, and so
whatever it prints will appear before what ε3vanilla-flavorε* prints.
So we can use before-daemons to add prefixes to a printed
representation; similarly, after-daemons can add suffixes.
There are other ways to combine methods besides daemons, but this way is the
most common. The more advanced ways of combining methods are explained
in a later section; see ⊗(method-combination). The ε3vanilla-flavorε* and what it does for
you are also explained later; see ⊗(vanilla-flavor).
.section Flavor Functions
.defmac defflavor
A flavor is defined by a form
.lisp
(defflavor ε2flavor-nameε* (ε2var1ε* ε2var2ε*...) (ε2flav1ε* ε2flav2ε*...)
ε2opt1ε* ε2opt2ε*...)
.end←lisp
ε2flavor-nameε* is a symbol which serves to name this flavor. It will get an ε3si:flavorε*
property of the internal data-structure containing the details of the flavor.
ε3(typep ε2objε*)ε*, where ε2objε* is an instance of the flavor named
ε2flavor-nameε*, will return the symbol ε2flavor-nameε*.
ε3(typep ε2objε* ε2flavor-nameε*)ε* is ε3tε* if ε2objε* is an instance of a
flavor, one of whose components (possibly itself) is ε2flavor-nameε*.
ε2var1ε*, ε2var2ε*, etc. are the names of the instance-variables
containing the local state for this flavor. A list of the name of an
instance-variable and a default initialization form is also acceptable;
the initialization form will be evaluated when an instance of
the flavor is created if no other initial value for
the variable is obtained. If no initialization is specified, the variable
will remain unbound.
ε2flav1ε*, ε2flav2ε*, etc. are the names of the component flavors out of
which this flavor is built. The features of those flavors are inherited
as described previously.
ε2opt1ε*, ε2opt2ε*, etc. are options; each option may be either a
keyword symbol or a list of a keyword symbol and arguments. The options
to ε3defflavorε* are described on ⊗(defflavor-options).
.end←defmac
.defvar *all-flavor-names*
This is a list of the names of all the flavors that have ever been ε3defflavorε*'ed.
.end←defvar
.defmac defmethod
A method, that is, a function to handle a particular message sent to
an instance of a particular flavor, is defined by a form such as
.lisp
(defmethod (ε2flavor-nameε* ε2method-typeε* ε2messageε*) ε2lambda-listε*
ε2form1ε* ε2form2ε*...)
.end←lisp
ε2flavor-nameε* is a symbol which is the name of the flavor which is to
receive the method. ε2method-typeε* is a keyword symbol for the type of method;
it is omitted when you are defining a primary method, which is the usual case.
ε2messageε* is a keyword symbol which names the message
to be handled.
The meaning of the ε2method-typeε* depends on what kind of
method-combination is declared for this message. For instance, for
daemons ε3:beforeε* and ε3:afterε* are allowed. See
⊗(method-combination) for a complete description of method types and
the way methods are combined.
ε2lambda-listε* describes the arguments and "aux variables" of the
function; the first argument to the method, which is the message
keyword, is automatically handled, and so it is not included in the
ε2lambda-listε*. Note that methods may not have ε3"eε* arguments;
that is they must be functions, not special forms. ε2form1ε*,
ε2form2ε*, etc. are the function body; the value of the last form
is returned.
The variant form
.lisp
(defmethod (ε2flavor-nameε* ε2messageε*) ε2functionε*)
.end←lisp
where ε2functionε* is a symbol, says that ε2flavor-nameε*'s method for
ε2messageε* is ε2functionε*, a symbol which names a function. That
function must take appropriate arguments; the first argument is the
message keyword.
If you redefine a method that is already defined, the old definition is
replaced by the new one. Given a flavor, a message name, and a method
type, there can only be one function, so if you define a ε3:beforeε*
daemon method for the ε3fooε* flavor to handle the ε3:barε* message,
then you replace the previous before-daemon; however, you do not affect
the primary method or methods of any other type, message name or flavor.
The function spec for a method (see ⊗(method-function-spec)) looks like:
.lisp
(:method ε2flavor-nameε* ε2messageε*) ε1orε*
(:method ε2flavor-nameε* ε2method-typeε* ε2messageε*)
.end←lisp
This is useful to know if you want to trace (⊗(trace-fun)) or advise (⊗(advise-fun))
a method, or if you want to poke around at the method function itself, e.g⊃.
disassemble (⊗(disassemble-fun)) it.
.cindex flavor-method-symbol
.setq flavor-method-symbol page
ε3defmethodε* actually defines a symbol, called the
ε2flavor-method-symbolε*, as a function, and the flavor system goes
through that symbol to call the method. The flavor-method-symbol is formed by
concatenating (with hyphens) the flavor name, the method type, the
message name, and ε3"method"ε* (for example, ε3ship-x-position-methodε*,
ε3ship-after-y-velocity-methodε*, ε3ship-combined-mass-methodε*, etc.).
The property list of this symbol is used to allow ε3undefunε* (⊗(undefun-fun))
and ε3uncompileε* (⊗(uncompile-fun)) to work. This is likely to be changed
in the future.
.end←defmac
.defun make-instance flavor-name init-option1 value1 init-option2 value2...
Creates and returns an instance of the specified flavor. Arguments after
the first are alternating init-option keywords and arguments to those keywords.
These options are used to initialize instance variables and to select
arbitrary options, as described above. If the flavor supports the ε3:initε*
message, it is sent to the newly-created object with one argument, the init-plist.
This is a disembodied property-list containing the init-options specified and
those defaulted from the flavor's ε3:default-init-plistε*. ε3make-instanceε*
is an easy-to-call interface to ε3instantiate-flavorε*; for full details
refer to that function.
.end←defun
.defun instantiate-flavor flavor-name init-plist &optional send-init-message-p return-unhandled-keywords area
This is an extended version of ε3make-instanceε*, giving you more features.
Note that it takes the init-plist as an argument, rather than taking a ε3&restε*
argument of init-options and values.
The ε2init-plistε* argument must be a disembodied property list;
ε3locfε* of a ε3&restε* argument will do. Beware! This property list
can be modified; the properties from the default-init-plist are
ε3putpropε*'ed on if not already present, and some ε3:initε* methods
do explicit ε3putpropε*s onto the init-plist.
In the event that ε3:initε* methods do ε3rempropε* of
properties already on the init-plist (as opposed to simply doing ε3getε*
and ε3putpropε*), then the init-plist will get
ε3rplacdε*'ed. This means that the actual list of options will be modified.
It also means that ε3locfε* of a ε3&restε* argument will not work; the
caller of ε3instantiate-flavorε* must copy its rest argument (e.g⊃. with ε3appendε*);
this is because ε3rplacdε* is not allowed on ε3&restε* arguments.
First, if the flavor's method-table and other internal information have
not been computed or are not up to date, they are computed. This may
take a substantial amount of time and invoke the compiler, but will
only happen once for a particular flavor no matter how many instances
you make, unless you change something.
Next, the instance variables are initialized. There are
several ways this initialization can happen.
If an instance variable is declared initable, and a keyword with
the same spelling as its name appears in ε2init-plistε*, it is set
to the value specified after that keyword. If an instance variable
does not get initialized this way, and an initialization form was
specified for it in a ε3defflavorε*, that form is evaluated and the
variable is set to the result. The initialization form may not depend
on any instance variables nor on ε3selfε*; it will not be evaluated in the "inside"
environment in which methods are called.
If an instance variable does not get initialized either of these ways
it will be left unbound; presumably an ε3:initε* method should initialize it
(see below). Note that a simple empty disembodied property list is
ε3(nil)ε*, which is what you should give if you want an empty init-plist.
If you use ε3nilε*, the property list of ε3nilε* will be used, which
is probably not what you want.
If any keyword appears in the ε2init-plistε* but is not used to
initialize an instance variable and is not declared in an
ε3:init-keywordsε* option (see ⊗(init-keywords-option)) it is presumed
to be a misspelling. So any keywords that you handle in an ε3:initε*
handler should also be mentioned in the ε3:init-keywordsε* option
of the definition of the flavor.
If the ε2return-unhandled-keywordsε* argument is
not supplied, such keywords are complained about by signalling an error.
But if ε2return-unhandled-keywordsε* is supplied non-ε3nilε*, a list of
such keywords is returned as the second value of ε3instantiate-flavorε*.
Note that default values in the ε2init-plistε* can come from
the ε3:default-init-plistε* option to ε3defflavorε*. See
⊗(default-init-plist-option).
If the ε2send-init-message-pε* argument is supplied and non-ε3nilε*, an
ε3:initε* message is sent to the newly-created instance, with one
argument, the ε2init-plistε*. ε3getε* can be used to extract options
from this property-list. Each flavor that needs initialization can
contribute an ε3:initε* method, by defining a daemon.
If the ε2areaε* argument is specified, it is the number of an area in which
to cons the instance; otherwise it is consed in the default area.
.end←defun
.defmac defwrapper
This is hairy and if you don't understand it you should skip it.
Sometimes the way the flavor system combines the methods of different
flavors (the daemon system) is not powerful enough. In that case ε3defwrapperε*
can be used to define a macro which expands into code which is wrapped around
the invocation of the methods. This is best explained by an example;
suppose you needed a lock locked during the processing of the
ε3:fooε* message to the ε3barε* flavor, which takes two arguments,
and you have a ε3lock-frobbozε* special-form which knows how to lock the lock
(presumably it generates an ε3unwind-protectε*). ε3lock-frobbozε* needs to see
the first argument to the message; perhaps that tells it what sort of operation
is going to be performed (read or write).
.lisp
(defwrapper (bar :foo) ((arg1 arg2) . body)
`(lock-frobboz (self arg1)
. ,body))
.end←lisp
The use of the ε3bodyε* macro-argument prevents the ε3defwrapperε*'ed
macro from knowing the exact implementation and allows several ε3defwrapperε*s
from different flavors to be combined properly.
Note well that the argument variables, ε3arg1ε* and ε3arg2ε*, are not referenced
with commas before them. These may look like ε3defmacroε* "argument" variables,
but they are not. Those variables are not bound at the time the ε3defwrapperε*-defined
macro is expanded and the back-quoting is done; rather the result of that
macro-expansion and back-quoting is code which, when a message is sent, will
bind those variables to the arguments in the message as local variables of
the combined method.
Consider another example. Suppose you thought you wanted a ε3:beforeε* daemon,
but found that if the argument was ε3nilε* you needed to return from processing
the message immediately, without executing the primary method. You could write
a wrapper such as
.lisp
(defwrapper (bar :foo) ((arg1) . body)
`(cond ((null arg1)) ;Do nothing if arg1 is nil
(t ε2before-codeε*
. ,body)))
.end←lisp
Suppose you need a variable for communication among the daemons for a particular
message; perhaps the ε3:afterε* daemons need to know what the primary method did,
and it is something that cannot be easily deduced from just the arguments. You
might use an instance variable for this, or you might create a special variable
which is bound during the processing of the message and used free by the methods.
.lisp
(defvar *communication*)
(defwrapper (bar :foo) (ignore . body)
`(let ((*communication* nil))
. ,body))
.end←lisp
Similarly you might want a wrapper which puts a ε3*catchε* around the processing
of a message so that any one of the methods could throw out in the event of
an unexpected condition.
If you change a wrapper, the change may not take effect automatically. You must
use ε3recompile-flavorε* with a third argument of ε3nilε* to force the effect
to propagate into the compiled code which the system generates to implement the
flavor. The reason for this is that the flavor system cannot reliably tell the
difference between reloading a file containing a wrapper and really redefining
the wrapper to be different, and propagating a change to a wrapper is expensive.
[This may be fixed in the future.]
Like daemon methods, wrappers work in outside-in order; when you add a
ε3defwrapperε* to a flavor built on other flavors, the new wrapper
is placed outside any wrappers of the component flavors. However,
ε2allε* wrappers happen before ε2anyε* daemons happen. When the combined
method is built, the calls to the before-daemon methods, primary methods,
and after-daemon methods are all placed together, and then the wrappers
are wrapped around them. Thus, if a component flavor defines a wrapper,
methods added by new flavors will execute within that wrapper's context.
.end←defmac
.defmac undefmethod
.lisp
(undefmethod (flavor :before :message))
ε1removes the method created byε*
(defmethod (flavor :before :message) (ε2argsε*) ...)
.end←lisp
To remove a wrapper, use ε3undefmethodε* with ε3:wrapperε* as the method type.
.end←defmac
.defvar self
When a message is sent to an object, the variable ε3selfε* is automatically
bound to that object, for the benefit of methods which want to manipulate
the object itself (as opposed to its instance variables).
.end←defvar
.defun funcall-self message arguments...
When ε3selfε* is an instance or an entity, ε3(funcall-self
ε2argsε*...)ε* has the same effect as ε3(funcall self ε2argsε*...)ε*
except that it is a little faster since it doesn't have to re-establish
the context in which the instance variables evaluate correctly. If
ε3selfε* is not an instance (nor an "entity", see ⊗(entity)),
ε3funcall-selfε* and ε3funcall selfε* do the same thing.
When ε3selfε* is an instance, ε3funcall-selfε* will only work correctly
if it is used in a method or a function, wrapped in a
ε3declare-flavor-instance-variablesε*, that was called (not necessarily
directly) from a method. Otherwise the instance-variables will not be
already set up.
.end←defun
.defun lexpr-funcall-self message arguments... list-of-arguments
This function is a cross between ε3lexpr-funcallε* and ε3funcall-selfε*.
When ε3selfε* is an instance or an entity, ε3(lexpr-funcall-self ε2argsε*...)ε* has the
same effect as ε3(lexpr-funcall self ε2argsε*...)ε*
except that it is a little faster since it doesn't have to re-establish
the context in which the instance variables evaluate correctly. If
ε3selfε* is not an instance (nor an "entity", see ⊗(entity)),
ε3lexpr-funcall-selfε* and ε3lexpr-funcallε* do the same thing.
.end←defun
.defmac declare-flavor-instance-variables
Sometimes you will write a function which is not itself a method, but
which is to be called by methods and wants to be able to access the
instance variables of the object ε3selfε*. The form
.lisp
(declare-flavor-instance-variables (ε2flavor-nameε*)
ε2function-definitionε*)
.end←lisp
surrounds the ε2function-definitionε* with a declaration of the
instance variables for the specified flavor, which will make them
accessible by name. Currently this works by declaring them as special
variables, but this implementation may be changed in the future.
Note that it is only legal to call a function defined this way while executing
inside a method for an object of the specified flavor, or of some
flavor built upon it.
.end←defmac
.defun recompile-flavor flavor-name &optional single-message (use-old-combined-methods ε3tε*) (do-dependents ε3tε*)
Updates the internal data of the flavor and any flavors that depend on it.
If ε2single-messageε* is supplied non-ε3nilε*, only the methods for that
message are changed. The system does this when you define a new method that
did not previously exist.
If ε2use-old-combined-methodsε* is ε3tε*, then the existing combined
method functions will be used if possible. New ones will only be generated
if the set of methods to be called has changed. This
is the default.
If ε2use-old-combined-methodsε* is ε3nilε*, automatically-generated functions
to call multiple methods or to contain code generated by wrappers will be regenerated
unconditionally.
If you change a wrapper, you must do ε3recompile-flavorε* with third argument ε3nilε*
in order to make the new wrapper take effect.
If ε2do-dependentsε* is ε3nilε*, only the specific flavor you specified
will be recompiled. Normally it and all flavors that depend on it will be recompiled.
ε3recompile-flavorε* only affects flavors that have already been compiled.
Typically this means it affects flavors that have been instantiated,
but does not bother with mixins (see ⊗(mixin-flavor)).
.end←defun
.defmac compile-flavor-methods
The form ε3(compile-flavor-methods ε2flavor-name-1ε*
ε2flavor-name-2ε*...)ε*, placed in a file to be compiled, will cause the
compiler to include the automatically-generated combined methods for the
named flavors in the resulting ε3qfaslε* file, provided all of the
necessary flavor definitions have been made. Furthermore, when the ε3qfaslε* file is
loaded, internal data structures (such as the list of all methods of a
flavor) will get generated.
This means that the combined methods get compiled at compile time, and
the data structures get generated at load time, rather than both things
happening at run time. This is a very good thing to use, since the
need to invoke the compiler at run-time makes programs that use flavors
slow the first time they are run. (The compiler will still be called if
incompatible changes have been made, such as addition or deletion of
methods that must be called by a combined method.)
You should only use ε3compile-flavor-methodsε* for flavors that are
going to be instantiated. For a flavor that will never be instantiated
(that is, a flavor that only serves to be a component of other flavors
that actually do get instantiated), it is a complete waste of time.
The ε3compile-flavor-methodsε* forms should be compiled after all of
the information needed to create the combined methods is available. You
should put these forms after all of the definitions of all relevant
flavors, wrappers, and methods of all components of the flavors mentioned.
When a ε3compile-flavor-methodsε* form is seen by the interpreter,
the combined methods are compiled and the internal data structures
are generated.
.end←defmac
.defun get-handler-for object message
Given an object and a message, will return that object's method for that
message, or ε3nilε* if it has none. When ε2objectε* is an instance of
a flavor, this function can be useful to find which of that flavor's
components supplies the method. If you get back a combined method,
you can use the List Combined Methods editor command (⊗(list-combined-methods))
to find out what it does.
This function can be used with other
things than flavors, and has an optional argument which is not relevant here
and not documented.
.end←defun
.defun flavor-allows-init-keyword-p flavor-name keyword
Returns non-ε3nilε* if the flavor named ε2flavor-nameε* allows ε2keywordε*
in the init options when it is instantiated, or ε3nilε* if it does not.
The non-ε3nilε* value is the name of the component flavor which contributes
the support of that keyword.
.end←defun
.defun symeval-in-instance instance symbol &optional no-error-p
This function is used to find the value of an instance variable
inside a particular instance. ε2Instanceε* is the instance to
be examined, and ε2symbolε* is the instance variable whose value
should be returned. If there is no such instance variable, an
error is signalled, unless ε2no-error-pε* is non-ε3nilε* in which
case ε3nilε* is returned.
.end←defun
.defun set-in-instance instance symbol value
This function is used to alter the value of an instance variable inside
a particular instance. ε2Instanceε* is the instance to be altered,
ε2symbolε* is the instance variable whose value should be set, and
ε2valueε* is the new value. If there is no such instance variable, an
error is signalled.
.end←defun
.defun locate-in-instance instance symbol
Returns a locative pointer to the cell inside ε2instanceε* which holds the
value of the instance variable named ε2symbolε*.
.end←defun
.defun describe-flavor flavor-name
This function prints out descriptive information about a flavor; it is
self-explanatory. An important thing it tells you that can be hard to
figure out yourself is the combined list of component flavors; this list
is what is printed after the phrase "and directly or indirectly depends
on".
.end←defun
.defvar si:*flavor-compilations*
This variable contains a history of when the flavor mechanism invoked
the compiler. It is a list; elements toward the front of the list
represent more recent compilations. Elements are typically of the
form
.lisp
(:method ε2flavor-name type message-nameε*)
.end←lisp
and ε2typeε* is typically ε3:combinedε*.
You may ε3setqε* this variable to ε3nilε* at any time; for instance before
loading some files that you suspect may have missing or obsolete
ε3compile-flavor-methodsε* in them.
.end←defvar
.section Defflavor Options
.setq defflavor-options section-page
There are quite a few options to ε3defflavorε*. They are all described here,
although some are for very specialized purposes and not of interest to most users.
Each option can be written in two forms; either the keyword by itself, or a list
of the keyword and "arguments" to that keyword.
Several of these options declare things about instance variables.
These options can be given with arguments which are instance variables,
or without any arguments in which case they refer to all of the
instance variables listed at the top of the ε3defflavorε*. This is
ε2notε* necessarily all the instance variables of the component
flavors; just the ones mentioned in this flavor's ε3defflavorε*. When
arguments are given, they must be instance variables that were listed
at the top of the ε3defflavorε*; otherwise they are assumed to be
misspelled and an error is signalled. It is legal to declare things
about instance variables inherited from a component flavor, but to do
so you must list these instance variables explicitly in the instance
variable list at the top of the ε3defflavorε*.
.table 3 0 500
.item :gettable-instance-variables
.setq gettable-instance-variables-option page
Enables automatic generation of methods for getting the values of
instance variables. The message name is the name of the variable, in
the keyword package (i.e⊃. put a colon in front of it.)
.item :settable-instance-variables
Enables automatic generation of methods for setting the values of
instance variables. The message name is "ε3:set-ε*" followed by the
name of the variable. All settable instance
variables are also automatically made gettable and initable.
.item :initable-instance-variables
The instance variables listed as arguments, or all instance variables
listed in this ε3defflavorε* if the keyword is given alone, are made
ε2initableε*. This means that they can be initialized through use of a
keyword (a colon followed by the name of the variable) as an init-option
argument to ε3make-instanceε*.
.item :init-keywords
.setq init-keywords-option page
The arguments are declared to be keywords in the initialization
property-list which are processed by this flavor's ε3:initε* methods.
The system uses this for error-checking: before the system sends the
ε3:initε* message, it makes sure that all the keywords in the
init-plist are either initable-instance-variables, or elements of this
list. If the caller misspells a keyword or otherwise uses a keyword
that no component flavor handles, this feature will signal an error.
When you write a ε3:initε* handler that accepts some keywords, they
should be listed in the ε3:init-keywordsε* option of the flavor.
.item :default-init-plist
.setq default-init-plist-option page
The arguments are alternating keywords and value forms, like a
property-list. When the flavor is instantiated, these properties and
values are put into the init-plist unless already present. This allows
one component flavor to default an option to another component flavor.
The value forms are only evaluated when and if they are used. For
example,
.lisp
(:default-init-plist :frob-array
(make-array 100))
.end←lisp
would provide a default "frob array" for any instance for which the
user did not provide one explicitly.
.item :required-instance-variables
.setq required-instance-variables-option page
Declares that any flavor incorporating this one which is instantiated
into an object must contain the specified instance variables.
An error occurs if there is an attempt to instantiate a flavor that
incorporates this one if it does not have these in its set of instance
variables. Note that this option is not one of those which checks
the spelling of its arguments in the way described at the start of this section
(if it did, it would be useless).
Required instance variables may be freely accessed by methods just like
normal instance variables. The difference between listing instance
variables here and listing them at the front of the ε3defflavorε* is
that the latter declares that this flavor "owns" those variables and
will take care of initializing them, while the former declares that this
flavor depends on those variables but that some other flavor must be
provided to manage them and whatever features they imply.
.item :required-methods
The arguments are names of messages which any flavor incorporating this
one must handle. An error occurs if there is an attempt to instantiate
such a flavor and it is lacking a method for one of these messages.
Typically this option appears in the ε3defflavorε* for a base flavor
(see ⊗(base-flavor)). Usually this is used when a base flavor
does a ε3funcall-selfε* (⊗(funcall-self-fun)) to send itself
a message that is not handled by the base flavor itself; the
idea is that the base flavor will not be instantiated alone, but
only with other components (mixins) that do handle the message.
This keyword allows the error of having no handler for the message
be detected when the flavor is defined (which usually means at compile time)
rather than at run time.
.item :included-flavors
.setq included-flavors-option page
The arguments are names of flavors to be included in this flavor. The difference
between declaring flavors here and declaring them at the top of the ε3defflavorε*
is that when component flavors are combined, all the included flavors come
after all the regular flavors. Thus included flavors act like defaults.
For an example of the use of included flavors, consider the ε3shipε*
example given earlier, and suppose we want to define a ε3relativity-mixinε*
which increases the mass dependent on the speed. We might write,
.lisp
(defflavor relativity-mixin () (moving-object))
(defmethod (relativity-mixin :mass) ()
(// mass (sqrt (- 1 (↑ (// (funcall-self ':speed)
*speed-of-light*)
2)))))
.end←lisp
but this would lose because any flavor that had ε3relativity-mixinε*
as a component would get ε3moving-objectε* right after it in its
component list. As a base flavor, ε3moving-objectε* should be last
in the list of components so that other components mixed in can replace
its methods and so that daemon methods combine in the right order.
So instead we write,
.lisp
(defflavor relativity-mixin () ()
(:included-flavors moving-object))
.end←lisp
which allows ε3relativity-mixinε*'s methods to access ε3moving-objectε*
instance variables such as ε3massε* (the rest mass), but does not
specify a place for ε3moving-objectε* in the list of components.
(Actually it puts it at the end, where it will usually be eliminated
as a duplicate, unless some other component flavor explicitly mentions
the included flavor as a component.)
.item :no-vanilla-flavor
Unless this option is specified, ε3si:vanilla-flavorε* is included (in
the sense of the ε3:included-flavorsε* option). ε3vanilla-flavorε*
provides some default methods for the ε3:print-selfε*, ε3:describeε*,
ε3:which-operationsε*, ε3:get-handler-forε*, ε3:eval-inside-yourselfε*,
and ε3:funcall-inside-yourselfε* messages. See ⊗(vanilla-flavor).
.item :default-handler
The argument is the name of a function which is to be called when a message
is received for which there is no method.
It will be called with whatever arguments the instance was called with,
including the message name; whatever values it returns will be returned.
If this option is not specified
on any component flavor, it defaults to a function which will signal an error.
.item :ordered-instance-variables
This option is mostly for esoteric internal system uses.
The arguments are names of instance variables which must appear first (and in this order)
in all instances of this flavor, or any flavor depending on this flavor.
This is used for instance variables which are specially known about by
microcode, and in connection with the ε3:outside-accessible-instance-variablesε*
option. If the keyword is given alone, the arguments default to the list
of instance variables given at the top of this ε3defflavorε*.
.item :outside-accessible-instance-variables
.setq outside-accessible-instance-variables-option page
The arguments are instance variables which are to be accessible from
"outside" of this object, that is from functions other than methods.
A macro (actually a ε3defsubstε*) is defined which takes an object of
this flavor as an argument and returns the value of the instance variable;
ε3setfε* may be used to set the value of the instance variable. The name
of the macro is the name of the flavor concatenated with a hyphen and the
name of the instance variable. These macros are similar to the accessor
macros created by ε3defstructε* (see ⊗(defstruct).)
This feature works in two different ways, depending on whether the instance
variable has been declared to have a fixed slot in all instances, via the
ε3:ordered-instance-variablesε* option.
If the variable is not ordered, the position of its value cell in the
instance will have to be computed at run time. This takes noticeable
time, although less than actually sending a message would take. An
error will be signalled if the argument to the accessor macro is
not an instance or is an instance which does not have an instance
variable with the appropriate name. However, there is no error check
that the flavor of the instance is the flavor the accessor macro was
defined for, or a flavor built upon that flavor. This error check
would be too expensive.
If the variable is ordered, the compiler will compile a call to
the accessor macro into a subprimitive which simply accesses that
variable's assigned slot by number. This subprimitive is only 3
or 4 times slower than ε3carε*. The only error-checking
performed is to make sure that the argument is really an instance
and is really big enough to contain that slot. There is no check
that the accessed slot really belongs to an instance variable of
the appropriate name. Any functions that use these accessor macros will
have to be recompiled if the number or order of instance
variables in the flavor is changed. The system will not
know automatically to do this recompilation. If you aren't very careful,
you may forget to recompile something, and have a very
hard-to-find bug. Because of this problem, and because using
these macros is less elegant than sending messages, the use of
this option is discouraged. In any case the use of these accessor macros
should be confined to the module which owns the flavor, and the "general
public" should send messages.
.item :accessor-prefix
Normally the accessor macro created by the ε3:outside-accessible-instance-variablesε*
option to access the flavor ε2fε*'s instance variable ε2vε* is named ε2f-vε*.
Specifying ε3(:accessor-prefix get$)ε* would cause it to be named ε3get$ε2vε*ε* instead.
.item :select-method-order
This is purely an efficiency hack due to the fact that currently the method-table
is searched linearly when a message is sent. The arguments are names of messages
which are frequently used or for which speed is important. Their methods
are moved to the front of the method table so that they are accessed
more quickly.
.item :method-combination
.setq method-combination-option page
Declares the way that methods from different flavors will be combined.
Each "argument" to this option is a list ε3(ε2type order message1 message2ε*...)ε*.
ε2Message1ε*, ε2message2ε*, etc. are names of messages whose methods
are to be combined in the declared fashion. ε2typeε* is a keyword which
is a defined type of combination; see ⊗(method-combination). ε2Orderε*
is a keyword whose interpretation is up to ε2typeε*; typically it is
either ε3:base-flavor-firstε* or ε3:base-flavor-lastε*.
Any component of a flavor may specify the type of method combination
to be used for a particular message. If no component specifies a type
of method combination, then the default type is used, namely ε3:daemonε*.
If more than one component of a flavor specifies it, then they must
agree on the specification, or else an error is signalled.
.item :documentation
The list of arguments to this option is remembered on the flavor's property
list as the ε3:documentationε* property. The (loose) standard for what can
be in this list is as follows; this may be extended in the future. A string
is documentation on what the flavor is for; this may consist of a brief
overview in the first line, then several paragraphs of detailed documentation.
A symbol is one of the following keywords:
.table 3 0 1300
.item :mixin
A flavor that you may want to mix with others to provide a useful feature.
.item :essential-mixin
A flavor that must be mixed in to all flavors of its class, or inappropriate
behavior will ensue.
.item :lowlevel-mixin
A mixin used only to build other mixins.
.item :combination
A combination of flavors for a specific purpose.
.item :special-purpose
A flavor used for some internal or kludgey purpose by a particular program,
which is not intended for general use.
.end←table
This documentation can be viewed with the ε3describe-flavorε* function
(see ⊗(describe-flavor-fun)) or the editor's ε3Meta-X Describe Flavorε*
command (see ⊗(describe-flavor-command)).
.end←table
.section Flavor Families
.setq base-flavor page
.setq mixin-flavor page
.cindex base-flavor
.cindex mixin
The following organization conventions are recommended for all programs that use flavors.
A ε2base flavorε* is a flavor that defines a whole family of related flavors,
all of which will have that base flavor as one of their components.
Typically the base flavor includes things relevant to the whole family,
such as instance variables, ε3:required-methodsε* and ε3:required-instance-variablesε*
declarations, default methods for certain messages, ε3:method-combinationε*
declarations, and documentation on the general protocols and conventions
of the family. Some base flavors are complete and can be instantiated, but
most are not instantiatable and merely serve as a base upon which to build
other flavors. The base flavor for the ε2fooε* family is often named ε3basic-ε2fooε*ε*.
A ε2mixin flavorε* is a flavor that defines one particular feature of an object.
A mixin cannot be instantiated, because it is not a complete description.
Each module or feature of a program
is defined as a separate mixin; a usable flavor can be constructed by choosing
the mixins for the desired characteristics and combining them, along with the
appropriate base flavor. By organizing your flavors this way, you keep separate
features in separate flavors, and you can pick and choose among them.
Sometimes the order of combining mixins does not matter,
but often it does, because the order of flavor combination controls the order
in which daemons are invoked and wrappers are wrapped. Such order dependencies
would be documented as part of the conventions of the appropriate family of flavors.
A mixin flavor that provides the ε2mumbleε* feature is often named ε2mumbleε3-mixinε*ε*.
If you are writing a program that uses someone else's facility to do something,
using that facility's flavors and methods, your program might still define
its own flavors, in a simple way. The facility might provide a base flavor and
a set of mixins, and the caller can combine these in various combinations depending
on exactly what it wants, since the facility probably would not provide all possible
useful combinations. Even if your private flavor has exactly the
same components as a pre-existing flavor, it can still be useful since
you can use its ε3:default-init-plistε* (see ⊗(default-init-plist-option)) to
select options of its component flavors and you can define one or two methods
to customize it "just a little".
.section Vanilla Flavor
.setq vanilla-flavor section-page
.defflavor si:vanilla-flavor
Unless you specify otherwise (with the ε3:no-vanilla-flavorε* option to
ε3defflavorε*), every flavor ε2includesε* the "vanilla" flavor, which has no
instance variables but provides some basic useful methods. Thus, nearly every
instance may be assumed to handle the following messages.
.end←defflavor
.defmessage :print-self stream prindepth slashify-p
The object should output its printed-representation to a stream. The
printer sends this message when it encounters an instance or an entity.
The arguments are the stream, the current depth in list-structure (for
comparison with ε3prinlevelε*), and whether slashification is enabled
(ε3prin1ε* vs ε3princε*; see ⊗(slashification)). Vanilla-flavor ignores
the last two arguments, and prints something like ε3#<ε2flavor-name
octal-addressε*>ε*. The ε2flavor-nameε* tells you what type of object
it is, and the ε2octal-addressε* allows you to tell different objects
apart (provided the garbage collector doesn't move them behind your back).
.end←defmessage
.defmessage :describe
The object should describe itself, printing a description onto
the ε3standard-outputε* stream. The ε3describeε*
function sends this message when it encounters an instance or an entity.
Vanilla-flavor outputs the object, the name of its flavor, and the names
and values of its instance-variables, in a reasonable format.
.end←defmessage
.defmessage :which-operations
The object should return a list of the messages it can handle.
Vanilla-flavor generates the list once per flavor and remembers it,
minimizing consing and compute-time. If a new method is added, the
list is regenerated the next time someone asks for it.
.end←defmessage
.defmessage :get-handler-for operation
The object should return the method it uses to handle ε2operationε*.
If it has no handler for that message, it should return ε3nilε*.
This is like the ε3get-handler-forε* function (see ⊗(get-handler-for-fun)),
but, of course, you can only use it on objects known to accept messages.
.end←defmessage
.defmessage :eval-inside-yourself form
The argument is a form which is evaluated in an environment in which special
variables with the names of the instance variables are bound to the values
of the instance variables. It works to ε3setqε* one of these special variables;
the instance variable will be modified. This is mainly intended to be used
for debugging. An especially useful value of ε2formε* is ε3(break t)ε*;
this gets you a Lisp top level loop inside the environment of the methods
of the flavor, allowing you to examine and alter instance variables, and
run functions that use the instance variables.
.end←defmessage
.defmessage :funcall-inside-yourself function &rest args
ε2functionε* is applied to ε2argsε* in an environment in which special
variables with the names of the instance variables are bound to the values
of the instance variables. It works to ε3setqε* one of these special variables;
the instance variable will be modified. This is mainly intended to be used
for debugging.
.end←defmessage
.section Method Combination
.setq method-combination section-page
As was mentioned earlier, there are many ways to combine methods. The
way we have seen is called the ε3:daemonε* type of combination. To use
one of the others, you use the ε3:method-combinationε* option to
ε3defflavorε* (see ⊗(method-combination-option)) to say that all the
methods for a certain message to this flavor, or a flavor built on it,
should be combined in a certain way.
The following types of method combination are supplied by the system. It is
possible to define your own types of method combination; for
information on this, see the code. Note that for most types of method
combination other than ε3:daemonε* you must define the order in which
the methods are combined, either ε3:base-flavor-firstε* or ε3:base-flavor-lastε*.
In this context, base-flavor means the last element of the flavor's
fully-expanded list of components.
Which method type keywords are allowed depends on the type of method
combination selected. Many of them allow only untyped methods. There
are also certain method types used for internal purposes.
.table 3
.item :daemon
This is the default type of method combination. All the ε3:beforeε*
methods are called, then the primary (untyped) method for the outermost
flavor that has one is called, then all the ε3:afterε* methods are
called. The value returned is the value of the primary method.
.item :progn
All the methods are called, inside a ε3prognε* special form.
No typed methods are allowed. This means that all of the
methods are called, and the result of the combined method is
whatever the last of the methods returns.
.item :or
All the methods are called, inside an ε3orε* special form.
No typed methods are allowed. This means that each of the
methods is called in turn. If a method returns a non-ε3nilε* value,
that value is returned and none of the rest of the methods are called;
otherwise, the next method is called. In other words, each method
is given a chance to handle the message; if it doesn't want to handle
the message, it should return ε3nilε*, and the next method will get
a chance to try.
.item :and
All the methods are called, inside an ε3andε* special form.
No typed methods are allowed. The basic idea is much like ε3:orε*;
see above.
.item :list
Calls all the methods and returns a list of their returned values. No typed
methods are allowed.
.item :inverse-list
Calls each method with one argument; these arguments are successive elements of the list which
is the sole argument to the message. No typed methods are allowed. Returns no
particular value. If the result of a ε3:listε*-combined message is
sent back with an ε3:inverse-listε*-combined message, with the same
ordering and with corresponding method definitions, each component
flavor receives the value which came from that flavor.
.end←table
Here is a table of all the method types used in the standard system (a user
can add more, by defining new forms of method-combination).
.table 3
.item ε1(no type)ε*
If no type is given to ε3defmethodε*, a primary method is created.
This is the most common type of method.
.item :before
.item1 :after
These are used for the before-daemon and after-daemon
methods used by ε3:daemonε* method-combination.
.item :default
If there are no untyped methods among any of the flavors being combined, then the
ε3:defaultε* methods (if any) are treated as if they were untyped. If there are
any untyped methods, the ε3:defaultε* methods are ignored.
Typically a base-flavor (see ⊗(base-flavor)) will define some default
methods for certain of the messages understood by its family. When
using the default kind of method-combination these default methods will
not be called if a flavor provides its own method. But with certain
strange forms of method-combination (ε3:orε* for example) the
base-flavor uses a ε3:defaultε* method to achieve its desired effect.
.item :wrapper
Used internally by ε3defwrapperε*.
.item :combined
Used internally for automatically-generated ε2combinedε* methods.
.end←table
The most common form of combination is ε3:daemonε*. One thing may not
be clear: when do you use a ε3:beforeε* daemon and when do you use an ε3:afterε*
daemon? In some cases the primary method performs a clearly-defined
action and the choice is obvious: ε3:before :launch-rocketε* puts in the
fuel, and ε3:after :launch-rocketε* turns on the radar tracking.
In other cases the choice can be less obvious. Consider the ε3:initε*
message, which is sent to a newly-created object. To decide what kind
of daemon to use, we observe the order in which daemon methods are
called. First the ε3:beforeε* daemon of the highest level of
abstraction is called, then ε3:beforeε* daemons of successively lower
levels of abstraction are called, and finally the ε3:beforeε* daemon (if
any) of the base flavor is called. Then the primary method is called.
After that, the ε3:afterε* daemon for the lowest level of abstraction is
called, followed by the ε3:afterε* daemons at successively higher levels
of abstraction.
Now, if there is no interaction among all these methods, if their
actions are completely orthogonal, then it doesn't matter whether you
use a ε3:beforeε* daemon or an ε3:afterε* daemon. It makes a difference
if there is some interaction. The interaction we are talking about is
usually done through instance variables; in general, instance variables
are how the methods of different component flavors communicate with each
other. In the case of the ε3:initε* message, the ε2init-plistε* can be
used as well. The important thing to remember is that no method knows
beforehand which other flavors have been mixed in to form this flavor; a
method cannot make any assumptions about how this flavor has been
combined, and in what order the various components are mixed.
This means that when a ε3:beforeε* daemon has run, it must assume that
none of the methods for this message have run yet. But the ε3:afterε*
daemon knows that the ε3:beforeε* daemon for each of the other flavors
has run. So if one flavor wants to convey information to the other, the
first one should "transmit" the information in a ε3:beforeε* daemon, and
the second one should "receive" it in an ε3:afterε* daemon. So while
the ε3:beforeε* daemons are run, information is "transmitted"; that is,
instance variables get set up. Then, when the ε3:afterε* daemons are
run, they can look at the instance variables and act on their values.
In the case of the ε3:initε* method, the ε3:beforeε* daemons typically set up
instance variables of the object based on the init-plist, while the
ε3:afterε* daemons actually do things, relying on the fact that all of the
instance variables have been initialized by the time they are called.
Of course, since flavors are not hierarchically organized, the notion of
levels of abstraction is not strictly applicable. However, it remains a
useful way of thinking about systems.
.section Implementation of Flavors
An object which is an instance of a flavor is implemented using the
data type ε3dtp-instanceε*. The representation is a structure whose
first word, tagged with ε3dtp-instance-headerε*, points to a structure
(known to the microcode as an "instance descriptor") containing the
internal data for the flavor. The remaining words of the structure are value cells
containing the values of the instance variables. The instance descriptor
is a ε3defstructε* which appears on the ε3si:flavorε* property of the flavor
name. It contains, among other things, the name of the flavor, the
size of an instance, the table of methods for handling messages, and
information for accessing the instance variables.
ε3defflavorε* creates such a data structure for each flavor, and
links them together according to the dependency relationships
between flavors.
A message is sent to an instance simply by calling it as a function,
with the first argument being the message keyword.
The microcode binds ε3selfε* to the object, binds the instance
variables (as ε3specialε* closure variables) to the value cells in
the instance, and calls a ε3dtp-select-methodε* associated with
the flavor. This ε3dtp-select-methodε* associates the message keyword
to the actual function to be called. If there is only one method,
this is that method, otherwise it is an automatically-generated function,
called the combined method (see ⊗(combined-method)),
which calls the appropriate methods in the right order.
If there are wrappers, they are incorporated into this combined
method.
The function-specifier syntax ε3(:method ε2flavor-name
optional-method-type message-nameε*)ε* is understood by ε3fdefineε* and
related functions. It is preferable to refer to methods this way
rather than by explicit use of the flavor-method-symbol (see ⊗(flavor-method-symbol)).
.subsection Order of Definition
There is a certain amount of freedom to the order in which you do ε3defflavorε*'s,
ε3defmethodε*'s, and ε3defwrapperε*'s. This freedom is designed to make it easy
to load programs containing complex flavor structures without having to do things
in a certain order. It is considered important that not all the methods for a flavor
need be defined in the same file. Thus the partitioning of a program into files
can be along modular lines.
The rules for the order of definition are as follows.
Before a method can be defined (with ε3defmethodε* or ε3defwrapperε*) its flavor
must have been defined (with ε3defflavorε*). This makes sense because the system
has to have a place to remember the method, and because it has to know the
instance-variables of the flavor if the method is to be compiled.
When a flavor is defined (with ε3defflavorε*) it is not necessary that
all of its component flavors be defined already. This is to allow
ε3defflavorε*'s to be spread between files according to the modularity
of a program, and to provide for mutually-included flavors (see the
ε3:included-flavorsε* ε3defflavorε* option, ⊗(included-flavors-option)).
Methods can be defined for a flavor some of whose component flavors are
not yet defined, however in certain cases compiling those methods will
produce a spurious warning that an instance variable was declared
special (because the system did not realize it was an instance
variable). In the current implementation these warnings may be ignored,
although that may not always be true in the future.
The methods automatically generated by the ε3:gettable-instance-variablesε*
and ε3:settable-instance-variablesε* ε3defflavorε* options
(see ⊗(gettable-instance-variables-option)) are generated at the time
the ε3defflavorε* is done.
The first time a flavor is instantiated, the system looks through all of
the component flavors and gathers various information. At this point an
error will be signalled if not all of the components have been
ε3defflavorε*'ed. This is also the time at which certain other errors
are detected, for instance lack of a required instance-variable (see the
ε3:required-instance-variablesε* ε3defflavorε* option,
⊗(required-instance-variables-option)). The combined methods (see
⊗(combined-method)) are generated at this time also, unless they already
exist. They will already exist if ε3compile-flavor-methodsε* was used,
but if those methods are obsolete because of changes made to component
flavors since the compilation, new combined methods will be made.
After a flavor has been instantiated, it is possible to make changes to it.
These changes will affect all existing instances if possible. This is
described more fully immediately below.
.subsection Changing a Flavor
You can change anything about a flavor at any time. You can change the flavor's
general attributes by doing another ε3defflavorε* with the same name. You
can add or modify methods by doing ε3defmethodε*'s. If you do a ε3defmethodε*
with the same flavor-name, message-name, and (optional) method-type as an existing
method, that method is replaced with the new definition. You can remove
a method with ε3undefmethodε* (see ⊗(undefmethod-fun)).
These changes will always propagate to all flavors that depend upon the
changed flavor. Normally the system will propagate the changes to all
existing instances of the changed flavor and all flavors that depend on
it. However, this is not possible when the flavor has been changed so
drastically that the old instances would not work properly with the new
flavor. This happens if you change the number of instance variables, which
changes the size of an instance. It also happens if you change the order
of the instance variables (and hence the storage layout of an instance),
or if you change the component flavors (which can change several subtle
aspects of an instance). The system does not keep a list of all the
instances of each flavor, so it cannot find the instances and modify them
to conform to the new flavor definition. Instead it gives you a warning
message, on the ε3error-outputε* stream, to the effect that the flavor
was changed incompatibly and the old instances will not get the new version.
The system leaves the old flavor data-structure intact (the old instances
will continue to point at it) and makes a new one to contain the new version
of the flavor. If a less drastic change is made, the system modifies the
original flavor data-structure, thus affecting the old instances that
point at it. However, if you redefine methods in such a way that they
only work for the new version of the flavor, then trying to use those
methods with the old instances won't work.
One exception to this is that changes to ε3defwrapperε*'s are never automatically
propagated. This is because doing so is expensive and the system cannot tell whether
you really changed it or just redefined it to be the same as it was. (Note that
the initial definition of a wrapper ε2isε* propagated, but redefinitions of it
are not.) See the documentation of ε3defwrapperε* for more details.
.subsection Restrictions
There is presently an implementation restriction that when using daemons,
the primary method may return at most three values if there are any ε3:afterε*
daemons. This is because the combined method needs a place to remember the
values while it calls the daemons. This will be fixed some day.
In this implementation, all message names must be in the keyword package,
in order for the flavor-method-symbols (see ⊗(flavor-method-symbol)) to
be unique, and for various tools in the editor to work correctly.
.section Entities
.setq flavor-entity section-page
An ε2entityε* is a Lisp object; the entity is one of the primitive
datatypes provided by the Lisp Machine system (the ε3data-typeε*
function (see ⊗(data-type-fun)) will return ε3dtp-entityε* if it is
given an entity). Entities are just like closures: they have all the
same attributes and functionality. The only difference between the two
primitive types is their data type: entities are clearly distinguished
from closures because they have a different data type. The reason there
is an important difference between them is that various parts of the
(not so primitive) Lisp system treat them differently. The Lisp functions
that deal with entities are discussed in ⊗(entity).
A closure is simply a kind of function, but
an entity is assumed to be a message-receiving object. Thus, when the
Lisp printer (see ⊗(printer)) is given a closure, it prints a simple
textual representation, but when it is handed an entity, it sends the
entity a ε3:print-selfε* message, which the entity is expected to
handle. The ε3describeε* function (see ⊗(describe-fun)) also sends
entities messages when it is handed them. So when you want to make a
message-receiving object out of a closure, as described on
⊗(entity-usage), you should use an entity instead.
Usually there is no point in using entities instead of flavors.
Entities were introduced into Lisp Machine Lisp before flavors were,
and perhaps they would not have been had flavors already existed.
Flavors have had considerably more attention paid to efficiency and
to good tools for using them.
Entities are created with the ε3entityε* function (see ⊗(entity-fun)).
The function part of an entity should usually be a function created
by ε3defselectε* (see ⊗(defselect-fun)).
.section Useful Editor Commands
Since we presently lack an editor manual, this section briefly documents
some editor commands that are useful in conjunction with flavors.
.table 3 0 500
.item meta-.
The ε3meta-.ε* (Edit Definition) command can find the definition of a flavor
in the same way that it can find the definition of a function.
Edit Definition can find the definition of a method if you give
.lisp
(:method ε2flavorε* ε2typeε* ε2messageε*)
.end←lisp
as the function name. The keyword ε3:methodε* may be omitted. Completion
will occur on the flavor name and message name as usual with Edit Definition.
.setq describe-flavor-command page
.item meta-X Describe Flavor
Asks for a flavor name in the mini-buffer and describes its characteristics.
When typing the flavor name you have completion over the names of all defined
flavors (thus this command can be used to aid in guessing the name of a flavor).
The display produced is mouse sensitive where there are names of flavors and
of methods; as usual the right-hand mouse button gives you a menu of operations
and the left-hand mouse button does the most common operation, typically positioning
the editor to the source code for the thing you are pointing at.
.item meta-X List Methods
.item1 meta-X Edit Methods
Asks you for a message in the mini-buffer and lists all the flavors
which have a method for that message. You may type in the message name,
point to it with the mouse, or let it default to the message which is
being sent by the Lisp form the cursor is inside of. List Methods
produces a mouse-sensitive display allowing you to edit selected methods
or just see which flavors have methods, while Edit Methods skips the
display and proceeds directly to editing the methods. As usual with
this type of command, the editor command ε3control-.ε* is redefined to
advance the editor cursor to the next method in the list, reading in its
source file if necessary. Typing ε3control-.ε* while the display is on
the screen edits the first method.
.item meta-X List Combined Methods
.item1 meta-X Edit Combined Methods
.setq list-combined-methods page
Asks you for a message and a flavor in two mini-buffers and lists all
the methods which would be called if that message were sent to an instance of that
flavor. You may point to the message and flavor with the mouse, and
there is completion for the flavor name.
As in List/Edit Methods, the display is mouse sensitive and
the Edit version of the command skips the display and proceeds directly
to the editing phase.
List Combined Methods can be very useful for telling what a flavor
will do in response to a message. It shows you the primary method,
the daemons, and the wrappers and lets you see the code for all of
them; type ε3control-.ε* to get to successive ones.
.end←table
ββ